package it.polimi.jita.cp.block.de;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.math3.fraction.BigFraction;

public class Histogram {
	private List<Bin> bins;
	private Map<Bin, BigFraction> cumulativeValues;
	private int binNumber;
	private final Integer lowerBound;
	private final Integer upperBound;
	private final BigFraction binWidth;
	private int sum;
	private static final Integer UNIT = 1;

	public Histogram(Integer binNumber, Integer lowerBound, Integer upperBound) {
		super();
		this.binNumber = binNumber;
		this.lowerBound = lowerBound;
		this.upperBound = upperBound;
		binWidth = new BigFraction(upperBound - lowerBound, binNumber);
		bins = initBins(binNumber, lowerBound, binWidth);
		cumulativeValues = new HashMap<>();
		sum = 0;
	}

	private List<Bin> initBins(Integer binNumber, Integer lowerBound,
			final BigFraction binWidth) {
		List<Bin> list = new ArrayList<>();

		BigFraction lb = new BigFraction(lowerBound);
		BigFraction ub = lb.add(binWidth);
		for (int i = 0; i < binNumber; i++) {
			list.add(new Bin(lb, ub, this));
			lb = ub;
			ub = lb.add(binWidth);
		}

		return list;
	}

	public Bin getBinByValue(double value) {
		if (value == upperBound)
			return bins.get(binNumber - 1);
		int id = (int) ((double) (value - lowerBound)
				/ (double) (upperBound - lowerBound) * (double) binNumber);
		return bins.get(id);
	}

	public Bin getBinByCumulativeValue(double value) {
		BigFraction lastCumulativeValue = new BigFraction(0);
		for (Bin bin : bins) {
			if (lastCumulativeValue.doubleValue() < value
					&& value < bin.getCumulativeValue().doubleValue())
				return bin;
			else
				lastCumulativeValue = bin.getCumulativeValue();
		}

		throw new RuntimeException("Value " + value
				+ " is out of range of cumulative values");
	}

	public void incrementBinValue(double value) {
		cumulativeValues.clear();
		if (value < lowerBound || value > upperBound)
			throw new RuntimeException("Value " + value
					+ " is not in the bound.");
		sum += UNIT;
		getBinByValue(value).incrementValue(UNIT);
	}

	public void incrementBinValueGEQ(double value) {
		cumulativeValues.clear();
		Bin firstBin = getBinByValue(value);
		Integer idBin = bins.indexOf(firstBin);
		BigFraction increment = new BigFraction(UNIT, (binNumber - idBin));
		sum++;
		for (int i = idBin; i < binNumber; i++) {
			bins.get(i).incrementValue(increment);
		}
	}

	public double sampleGEQ(double geq) {
		Bin binLb = getBinByValue(geq);

		double zeroFrequency = binLb.getCumulativeValue()
				.subtract(binLb.getValue()).doubleValue()
				+ (geq - binLb.getLowerBound().doubleValue())
				/ binWidth.doubleValue() * binLb.getValue().doubleValue();

		double choosenFrequency = zeroFrequency + (sum - zeroFrequency)
				* Math.random();
		Bin chooseBin = getBinByCumulativeValue(choosenFrequency);
		double choosenBinFreqLb = chooseBin.getCumulativeValue()
				.subtract(chooseBin.getValue()).doubleValue();

		double value = chooseBin.getLowerBound().doubleValue()
				+ (choosenFrequency - choosenBinFreqLb)
				/ chooseBin.getValue().doubleValue() * binWidth.doubleValue();

		return value;
	}

	public double[] sampleGEQ(int sampleNumber, double geq) {
		double[] samples = new double[sampleNumber];
		for (int i = 0; i < samples.length; i++) {
			samples[i] = sampleGEQ(geq);
		}
		return samples;
	}

	public int getSum() {
		return sum;
	}

	public List<Bin> getBins() {
		return Collections.unmodifiableList(bins);
	}

	private BigFraction getCumulativeValue(Bin bin) {
		if (!cumulativeValues.containsKey(bin)) {

			int binId = bins.indexOf(bin);
			if (binId == 0) {
				cumulativeValues.put(bin, bin.getValue());
			} else {
				cumulativeValues.put(
						bin,
						bin.getValue().add(
								getCumulativeValue(bins.get(binId - 1))));
			}
		}

		return cumulativeValues.get(bin);
	}

	public double cdf(double value) {
		return getBinByValue(value).getCumulativeValue(value) / (double) sum;
	}

	public static class Bin {
		private final Histogram histogram;
		private BigFraction value;
		private BigFraction upperBound;
		private BigFraction lowerBound;

		protected Bin(BigFraction lowerBound, BigFraction upperBound,
				Histogram histogram) {
			super();
			if (upperBound.compareTo(lowerBound) < 0)
				throw new RuntimeException(
						"In a Bin upper-bound must be equal or greater than lower-bound.");
			if (histogram == null)
				throw new RuntimeException("Histogram can not be null.");

			this.upperBound = upperBound;
			this.lowerBound = lowerBound;
			this.histogram = histogram;
			value = new BigFraction(0);
		}

		public BigFraction getValue() {
			return value;
		}

		public BigFraction getCumulativeValue() {
			return histogram.getCumulativeValue(this);
		}

		public BigFraction getPercentage() {
			return value.divide(histogram.getSum());
		}

		public BigFraction getUpperBound() {
			return upperBound;
		}

		protected BigFraction getLowerBound() {
			return lowerBound;
		}

		protected void incrementValue(BigFraction BigFraction) {
			value = value.add(BigFraction);
		}

		protected void incrementValue(int number) {
			value = value.add(number);
		}

		private double getCumulativeValue(double criticalSl) {
			double value = getCumulativeValue().doubleValue();

			value += getValue().doubleValue()
					* (criticalSl - lowerBound.doubleValue())
					/ histogram.binWidth.doubleValue();
			return value;
		}
	}

}
